feat(voice): global speak-selection hotkey + Settings row#127
Merged
Conversation
A configurable hotkey (STACKNUDGE_SPEAK_HOTKEY, default cmd+opt+s) reads the current selection aloud via stackvox — in any app, not just the browser. - SpeakSelection: captures the focused element's AX selected-text, falling back to synthesizing Cmd-C + reading the pasteboard (then restoring it) for apps that don't expose AX. Speaks via Speaker with --normalize; re-pressing (or pressing with nothing selected) cancels the current read. - Hotkey: parameterize the hotkey id so a second global hotkey can register, and gate each handler on its own id (handlers on the app target see all hotkey events). - Speaker: add a --normalize option and cancel() (talks to the daemon socket via nc, so it works regardless of installed CLI version; no-op if the daemon predates `cancel`). - Config: STACKNUDGE_SPEAK_HOTKEY. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The id-gating handler returned noErr even on a non-matching id. In Carbon that marks the event handled and stops chain propagation, so with two hotkeys registered whichever handler ran first consumed the event and the other hotkey (e.g. ⌘⌥N to open the panel) stopped firing. Return eventNotHandledErr on a non-match so the event reaches the handler whose id matches. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Surface and configure the speak-selection hotkey in Settings, mirroring the panel-hotkey recorder: a "Read aloud shortcut" row in the Voice section shows the current binding and records a new one (persists STACKNUDGE_SPEAK_HOTKEY, re-registers via setSpeakHotkey). - SettingsRow.speakHotkey + placement in the Voice group. - Recorder state (recordingSpeakHotkey / speakHotkeyDisplay / speakHotkeyError) and start/cancel/commit, mirroring the panel hotkey. - Panel key-capture routes the recorded combo to whichever hotkey is recording; registerSpeakHotkey re-registers the id:2 global hotkey. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
StuBehan
force-pushed
the
feat/speak-selection-hotkey
branch
from
July 13, 2026 17:49
21ef58e to
b19e9bc
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A global "speak selection" hotkey that reads the current selection aloud via stackvox — in any app. This is the "read anything, anywhere" surface alongside speaklast (reads Claude in the terminal) and the existing voice cues, all sharing the one local Kokoro voice.
Changes
SpeakSelection(panel/SpeakSelection.swift): captures the selection from the focused element'skAXSelectedTextAttribute, falling back to synthesizing ⌘C + reading the pasteboard (and restoring it) for apps that don't expose AX. Speaks it throughSpeakerwith--normalize; re-pressing (or pressing with nothing selected) cancels the current read.STACKNUDGE_SPEAK_HOTKEY(defaultcmd+opt+s) + a "Read aloud shortcut" recorder row in Settings › Voice, mirroring the panel-hotkey recorder (record / persist / re-register live).Hotkeynow supports a second global hotkey: parameterized id, and each handler gates on its own id — returningeventNotHandledErron a non-match so the two don't swallow each other's events (the panel hotkey keeps working).Speaker: a--normalizeoption andcancel()(sends the daemoncancelcommand over the socket).Testing
swiftc -typecheck panel/*.swift shared/*.swift— clean (0 errors).cmd+opt+s; select text in an app and press it → it reads; record a new combo → persists + works; and ⌘⌥N (panel) + Settings ↑/↓/Esc still behave while recording.Related issues
The stop/interrupt (re-press) uses stackvox's
cancelcommand, which shipped in stackvox 0.10.0 — so once the bundled daemon is on 0.10.0 (see the floor-bump PR), interrupting a read works; on older daemons it degrades to a no-op. Speaking works regardless.